﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Duellum.Core.ExceptionHelper;
using JpLabs.DynamicCode.Emit;
using Ninject;
using Ninject.Parameters;
using JpLabs.Extensions;

namespace Duellum.Core
{
	public static class DuelAssemblyParser
	{
		static public IEnumerable<KeyValuePair<Type,DuelTypeAttribute>> GetTypesWithDuelTypeAttrib(Assembly asm)
		{
			return (
				from type in asm.GetExportedTypes()
				where type.IsSubclassOf(typeof(AugObject))
				let attrib = type.GetSingleAttrOrNull<DuelTypeAttribute>(false)
				where attrib != null
				select new KeyValuePair<Type,DuelTypeAttribute>(type, attrib)
					//(attr.ElementName.IsNullOrEmpty()) ? type.Name : attr.ElementName, attrib
			);
		}
		
		static public IEnumerable<FieldInfo> GetFieldsHoldingAugProperties(Assembly asm)
		{
			return (
				from type in asm.GetExportedTypes()
				from field in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.GetField)
				where typeof(AugProperty).IsAssignableFrom(field.FieldType)
				select field
			);
		}
	}
	
	public sealed class AugDomain
	{
		static public readonly AugDomain Current;
		static AugDomain() { Current = new AugDomain(); }

		private readonly HashSet<string> assembliesScannedForProperties;
		private readonly HashSet<AugProperty> registeredProperties;
		
		private AugDomain()
		{
			assembliesScannedForProperties = new HashSet<string>();
			registeredProperties = new HashSet<AugProperty>();
			
			foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) RegisterAugPropertiesFromAssembly(asm);
			
			AppDomain.CurrentDomain.AssemblyLoad += CurrentDomain_AssemblyLoad;
		}

		void CurrentDomain_AssemblyLoad(object sender, AssemblyLoadEventArgs args)
		{
			RegisterAugPropertiesFromAssembly(args.LoadedAssembly);
		}

		private void RegisterProperty(AugProperty ap)
		{
			if (!registeredProperties.Add(ap)) throw Error.AugPropertyAlreadyRegistered(ap);
			
			cachedPropertiesByName = null;
		}
		
		ILookup<string,AugProperty> cachedPropertiesByName;
		internal ILookup<string,AugProperty> PropertiesByName
		{
			get {
				if (cachedPropertiesByName == null) cachedPropertiesByName = registeredProperties.ToLookup(p => p.Name);
				return cachedPropertiesByName;
			}
		}
		
		public AugProperty FindProp(string name, Type ownerType)
		{
			IEnumerable<AugProperty> q
				= PropertiesByName[name]
				.Where(ap => ap.OwnerType.IsAssignableFrom(ownerType))
				.TopologicalOrderBy(ap => ap.OwnerType, TypeComparer.Instance);
			
			//TODO: TEST property overriding
			//If a class "overrides" a property, the most specific should be returned
			//Considering that subclasses are smaller than base classes, I'll return the smallest			
			return q.FirstOrDefault();
		}

		public AugProperty FindProp(string name, string ownerName, params string[] namespaces)
		{
			IEnumerable<AugProperty> q
				= PropertiesByName[name]
				.Where(ap => ap.OwnerType.Name == ownerName);
			
			if (namespaces != null && namespaces.Length != 0) q = q.Where(ap => namespaces.Contains(ap.OwnerType.Namespace));
			
			return q.SingleOrDefault();
		}

		private void RegisterAugPropertiesFromAssembly(Assembly asm)
		{
            var companyAttr = asm.GetSingleAttrOrNull<AssemblyCompanyAttribute>(false);
            if (companyAttr != null && companyAttr.Company == "Microsoft Corporation") return; //ignore MS assemblies
            
            if (asm is System.Reflection.Emit.AssemblyBuilder) return; //ignore dynamic assemblies

		    var clrFieldsWithAugProperties = DuelAssemblyParser.GetFieldsHoldingAugProperties(asm);
			
		    var augProperties = clrFieldsWithAugProperties.Select( p => (AugProperty)p.GetValue(null) );
			
		    foreach (var ap in augProperties)
		    {
		        this.RegisterProperty(ap);
		    }
		}
	}
	
	public sealed class DuelDomain
	{
		
		private readonly object syncRoot = new object();
		
		private readonly IDictionary<DuelPluginName,IDuelPlugin> registeredPlugins;
		private readonly HashSet<string> assembliesScannedForPluginTypes;
		
		private readonly IDictionary<DuelTypeId,DuelType> registeredDuelTypes;
		
		private readonly IKernel duelTypeDeserializationKernel; 
		
		static public DuelDomain Create()
		{
		    return new DuelDomain();
		}
		
		private DuelDomain()
		{
			registeredPlugins = new Dictionary<DuelPluginName,IDuelPlugin>();
			assembliesScannedForPluginTypes = new HashSet<string>();
			registeredDuelTypes = new Dictionary<DuelTypeId,DuelType>();

			//assembliesScannedForProperties = new HashSet<string>();
			//registeredProperties = new HashSet<AugProperty>();
			
			//deserializerDict = new Dictionary<string,Type>();
			//factoryDictionary = new Dictionary<XName,Func<DuelTypeId,DuelType>>();
			duelTypeDeserializationKernel = new StandardKernel();
			
			//System.Runtime.Remoting.Messaging.CallContext
		}
		
		public bool HasPluginsAttached
		{
			get { return registeredPlugins.Count > 0; }
		}
		
		public void AttachPlugins(IEnumerable<DuelPluginName> names)
		{
			lock (syncRoot)
			{
				//TODO: Load plugins in order depending on references (or load everything in a way that the order is irrelevant)
				//TODO: What to do with circular dependencies?
				//proxies = proxies.OrderBy(???, ???);

				//Ensures that the RootDuelType is defined
				EnsureRootTypeExists();
				
				//Get proxies for all plugins
				var proxies = names.Select(n => n.CreateProxy(this)).ToArray();
				
				//Add all plugins
				foreach (var p in proxies) RegisterPlugin(p.Name, p);
				
				//Add all duelTypes
				var newTypes = proxies.SelectMany(p => p.GetTypes()).ToArray();
				foreach (var t in newTypes) InitAndRegisterType(t);
				
				//Ensure that all referenced duelTypes were registered
				foreach (var t in newTypes) t.EndInit();
				
				//Apply all decorations
				var decs = proxies.SelectMany(p => p.GetDecorations());
				foreach (var d in decs) ApplyDecoration(d);
				
				//EndInit on all plugins
				foreach (var p in proxies) p.EndInit();
				
				EnforceDuelTypeHierarchy();
			}
		}

		private void RegisterPlugin(DuelPluginName name, IDuelPlugin plugin)
		{
			lock (syncRoot)
			{
				if (registeredPlugins.ContainsKey(name)) throw Error.DuelPluginAlreadyAttached(name);
				
				plugin.BeginInit();
				
				registeredPlugins.Add(name, plugin);
			}
			
			ParsePluginAssembly(name, plugin);
		}
		
		private void ParsePluginAssembly(DuelPluginName pluginName, IDuelPlugin plugin)
		{
			Assembly asm = pluginName.PluginType.Assembly;

			//TODO: plugins could be more selective about the properties they want to register
			// For example, only registered types (DuelType types and IDuelPlugin types) could be considered
			
			//var temp = AppDomain.CreateDomain("temp");
			//RegisterAugPropertiesFromAssemblyAndReferences(asm);//, temp);
			//AppDomain.Unload(temp);
				
			if (assembliesScannedForPluginTypes.Add(asm.FullName))
			{
				RegisterBaseDuelTypesFromAssembly(asm);
			}
		}

		//static byte[] MSPublicToken = new byte[] {183, 122, 92, 86, 25, 52, 224, 137};

		//private void RegisterAugPropertiesFromAssemblyAndReferences(Assembly asm)//, AppDomain tempAppDomain)
		//{
		//    if (assembliesScannedForProperties.Add(asm.FullName))
		//    {
		//        RegisterAugPropertiesFromAssembly(asm);

		//        //AssemblyName
				
		//        var refAsms
		//            = asm.GetReferencedAssemblies()
		//            .Where( name => !name.GetPublicKeyToken().SequenceEqual(MSPublicToken) )
		//            //.Select( name => Assembly.ReflectionOnlyLoad(name.ToString()) )
		//            .Select( name => AppDomain.CurrentDomain.Load(name) )
		//            .Where(
		//                refAsm => {
		//                    var companyAttr = refAsm.GetSingleAttrOrNull<AssemblyCompanyAttribute>(false);
		//                    return companyAttr == null || companyAttr.Company != "Microsoft Corporation";
		//                }
		//            );
				
		//        foreach (var refAsm in refAsms) {
		//            RegisterAugPropertiesFromAssemblyAndReferences(refAsm);//, tempAppDomain);
		//        }
		//    }
		//}

		//private void RegisterAugPropertiesFromAssembly(Assembly asm)
		//{
		//    var clrFieldsWithAugProperties = DuelAssemblyParser.GetFieldsHoldingAugProperties(asm);
			
		//    var augProperties = clrFieldsWithAugProperties.Select( p => (AugProperty)p.GetValue(null) );
			
		//    foreach (var ap in augProperties)
		//    {
		//        this.RegisterProperty(ap);
		//    }
		//}

		private void RegisterBaseDuelTypesFromAssembly(Assembly asm)
		{
			var typesWithDuelTypeAttrib = DuelAssemblyParser.GetTypesWithDuelTypeAttrib(asm);
			
			//TODO: add base types to domain (this removes the need to declare them at xml level)
			// What if the plugin doesn't want to add these types?
			
			foreach (var typeAtrribPair in typesWithDuelTypeAttrib)
			{
				var type = typeAtrribPair.Key;
				var attrib = typeAtrribPair.Value;
				
				XName xName = attrib.XmlElementName;
				//var constructorFunc = GetDuelTypeConstructorFunc(typeAtrribPair.Key, typeAtrribPair.Value);
				
				//factoryDictionary.Add(xName, constructorFunc);
				duelTypeDeserializationKernel
					.Bind<DuelType>()
					.To(type)
					//.InScope( ctx => ctx.Request.Parameters.First().GetValue(ctx) )
					.Named(xName.ToString())
					.WithConstructorArgument(
						"id",
						ctx => ctx.Request.Parameters.First().GetValue(ctx)
					);
			}
		}
		
		//private static Func<DuelTypeId,DuelType> GetDuelTypeConstructorFunc(Type type, DuelTypeAttribute typeAttrib)
		//{
		//    if (type == null) throw new ArgumentNullException("type");
			
		//    //Get type constructor
		//    ConstructorInfo constructor = type.GetConstructor(new [] { typeof(DuelTypeId) });
		//    if (constructor == null) throw new ArgumentException("type does not have a constructor with one DuelTypeId parameter");
			
		//    //Create late bound creator function
		//    DynamicStaticMethod creator = LateBoundMethodFactory.CreateConstructor(constructor);
			
		//    //Return encapsulated creator function
		//    return (Func<DuelTypeId,DuelType>)(
		//        id => (DuelType)creator(id)
		//            //var type = (DuelType)creator(id);
		//            //type.SetParentId(typeAttrib.
		//    );
		//}

		internal DuelType CreateDuelTypeFromXElement(XElement x)
		{
			if (x == null) throw new ArgumentNullException("x");
			
			//Func<DuelTypeId,DuelType> constructorFunc;
			//if (!factoryDictionary.TryGetValue(x.Name, out constructorFunc)) {
			//    throw new InvalidOperationException("Constructor for <" + x.Name.ToString() + "> not defined");
			//}
			
			var idAttrib = x.Attribute("id");
			if (idAttrib == null) throw new ArgumentException("element doesn't have an 'id' attribute");			
			DuelTypeId id = (DuelTypeId)idAttrib.Value;
			
			//return constructorFunc(id);
			return duelTypeDeserializationKernel.Get<DuelType>(x.Name.ToString(), new Parameter("id", id, false));
		}
		
		private void InitAndRegisterType(DuelType type)
		{
			if (type == null) throw new ArgumentNullException("type");
			
			type.BeginInit();
			type.Domain = this;
			registeredDuelTypes.Add(type.Id, type);
		}

		private void EnsureRootTypeExists()
		{
			lock (syncRoot)
			{
				if (!registeredDuelTypes.ContainsKey(DuelRootType.TypeId)) {
					var root = new DuelRootType();
					InitAndRegisterType(root);
					root.EndInit();
				}
			}
		}

		private void EnforceDuelTypeHierarchy()
		{
			lock (syncRoot) {
				//Not needed anymore
				////0. Enforce root type on parent-less types
				//DuelType rootType = registeredTypes[DuelRootType.TypeId];
				//var rootlessTypes = registeredTypes.Values.Where(t => t.BaseType == null && t.Id != DuelRootType.TypeId);
				//foreach (var t in rootlessTypes) t.BaseType = rootType;
				
				//TODO: check if hierachy is a single tree (no disconnected types, and no loops)
			}
		}
		
		private void ApplyDecoration(DuelDecoration decoration)
		{
			if (decoration == null) throw new ArgumentNullException("decoration");
			
			DuelType type;
			if (!TryGetType(decoration.Id, out type)) throw Error.DuelTypeIsNotRegistered(decoration.Id);
			type.ApplyDecoration(decoration);
		}

		public T GetPlugin<T>() where T : class
		{
			var name = DuelPluginName.FromType(typeof(T));
			if (name == null) throw Error.TypeIsNotADuelPlugin(typeof(T));
			return (T)GetPlugin(name);
		}
		
		public IDuelPlugin GetPlugin(DuelPluginName name)
		{
			IDuelPlugin plugin;
			if (!registeredPlugins.TryGetValue(name, out plugin)) return null; //throw Error.DuelPluginNotAttached(name);
			return (plugin is DuelPluginProxy) ? ((DuelPluginProxy)plugin).Instance : plugin;
		}

		public bool TryGetType(DuelTypeId typeId, out DuelType outDuelType)
		{
		    return registeredDuelTypes.TryGetValue(typeId, out outDuelType);
		}

		public IEnumerable<DuelType> GetTypes()
		{
			return registeredDuelTypes.Values.ToArray();
		}

		public DuelType GetType(DuelTypeId typeId)
		{
			DuelType value;
		    if (registeredDuelTypes.TryGetValue(typeId, out value)) {
				return value;
		    } else {
				throw Error.DuelTypeNotFound(typeId);
		    }
		}

		//public DuelType CreateDynamicType(DuelTypeId parentTypeId)
		//{
		//    return CreateDynamicType(GetType(parentTypeId));
		//}

		//public DuelType CreateDynamicType(DuelType parentType)
		//{
		//    var randomGuid = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
		//    DuelTypeId newId = string.Concat(parentType.Id.ToString(), ".", suffix);
		//    return CreateDynamicType(parentType, newId);
		//}

		public DuelType GetOrCreateDynamicType(DuelType parentType, DuelTypeId newId)
		{
			bool newCreated;
			return GetOrCreateDynamicType(parentType, newId, out newCreated);
		}
		
		public DuelType GetOrCreateDynamicType(DuelType parentType, DuelTypeId newId, out bool newCreated)
		{
			lock (syncRoot) {
				DuelType registeredType;
				if (registeredDuelTypes.TryGetValue(newId, out registeredType)) {
					newCreated = false;
					return registeredType;
				} else {
					newCreated = true;
					return CreateDynamicDuelType(newId, parentType);
				}
			}
		}

		internal DuelType CreateDynamicDuelType(DuelTypeId id, DuelType parent)
		{
			lock (syncRoot) {
				var newType = new DynamicDuelType(id, parent);
				InitAndRegisterType(newType);
				return newType;
			}
		}

		public BaseDuelObj CreateObj(DuelTypeId typeId)
		{
			return GetType(typeId).CreateObj();
		}
	}
}
